摘要
文章参考自: 此处,记录了 Python 内置 logger 和 常见的日志库的使用,包括 Loguru,Structlog, Eliot, Logbook, Mirosoft 的 Picologging
标准日志模块
1 2 3 4 5 6 7 8 9 10 11 12 13
| import logging
logging.debug("A debug message") logging.info("An info message") logging.warning("A warning message") logging.error("An error message") logging.critical("A critical message")
""" WARNING:root:A warning message ERROR:root:An error message CRITICAL:root:A critical message """
|
自定义 Logger
1 2 3 4
| import logging
logger = logging.getLogger(__name__) ...
|
一旦有了自定义的 Logger,就可以使用Handler、 Formatter和Filter自定义其输出。
以下是使用自定义 Logger 记录到控制台和文件的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import sys import logging
logger = logging.getLogger("example") logger.setLevel(logging.DEBUG)
stdoutHandler = logging.StreamHandler(stream=sys.stdout) errHandler = logging.FileHandler("error.log")
stdoutHandler.setLevel(logging.DEBUG) errHandler.setLevel(logging.ERROR)
fmt = logging.Formatter("%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s")
stdoutHandler.setFormatter(fmt) errHandler.setFormatter(fmt)
logger.addHandler(stdoutHandler) logger.addHandler(errHandler)
logger.info("服务器在端口8080上开始侦听") logger.warning("驱动器'varlog'上的磁盘空间不足。考虑腾出空间")
try: raise Exception("连接数据库“my_db”失败") except Exception as e: logger.error(e, exc_info=True)
|
stdout 输出结果
包含 info,warning 等级的日志
1 2 3 4 5 6 7
| example: 2023-07-31 13:59:40,921 | INFO | aa.py:27 | 14384 >>> 服务器在端口8080上开始侦听 example: 2023-07-31 13:59:40,921 | WARNING | aa.py:28 | 14384 >>> 驱动器'varlog'上的磁盘空间不足。考虑腾出空间 example: 2023-07-31 13:59:40,921 | ERROR | aa.py:34 | 14384 >>> 连接数据库“my_db”失败 Traceback (most recent call last): File "D:\project\knowledge-api\aa.py", line 31, in <module> raise Exception("连接数据库“my_db”失败") Exception: 连接数据库“my_db”失败
|
日志文件输出结果
只包含 error 等级的日志
1 2 3 4 5
| example: 2023-07-31 13:52:57,416 | ERROR | aa.py:34 | 76 >>> 连接数据库“my_db”失败 Traceback (most recent call last): File "D:\project\knowledge-api\aa.py", line 31, in <module> raise Exception("连接数据库“my_db”失败") Exception: 连接数据库“my_db”失败
|
结构化输出
logging
除非您 实现一些额外的代码,否则该模块无法生成结构化日志。有一种更简单、更好的方法来获取结构化输出: python-json-logger 库:
1
| pip install python-json-logger
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import sys import logging
from pythonjsonlogger import jsonlogger
logger = logging.getLogger("example") logger.setLevel(logging.DEBUG)
stdoutHandler = logging.StreamHandler(stream=sys.stdout) errHandler = logging.FileHandler("error.log")
stdoutHandler.setLevel(logging.DEBUG) errHandler.setLevel(logging.ERROR)
fmt = jsonlogger.JsonFormatter( "%(name)s %(asctime)s %(levelname)s %(filename)s %(lineno)s %(process)d %(message)s", rename_fields={"levelname": "等级", "asctime": "日期"}, )
stdoutHandler.setFormatter(fmt) errHandler.setFormatter(fmt)
logger.addHandler(stdoutHandler) logger.addHandler(errHandler)
logger.info("服务器在端口8080上开始侦听") logger.warning("驱动器'varlog'上的磁盘空间不足。考虑腾出空间")
try: raise Exception("连接数据库“my_db”失败") except Exception as e: logger.error(e, exc_info=True)
|
stdout 输出结果
1 2 3
| {"name": "example", "filename": "aa.py", "lineno": 33, "process": 8148, "message": "\u670d\u52a1\u5668\u5728\u7aef\u53e38080\u4e0a\u5f00\u59cb\u4fa6\u542c", "\u7b49\u7ea7": "INFO", "\u65e5\u671f": "2023-07-31 14:19:08,683"} {"name": "example", "filename": "aa.py", "lineno": 34, "process": 8148, "message": "\u9a71\u52a8\u5668'varlog'\u4e0a\u7684\u78c1\u76d8\u7a7a\u95f4\u4e0d\u8db3\u3002\u8003\u8651\u817e\u51fa\u7a7a\u95f4", "\u7b49\u7ea7": "WARNING", "\u65e5\u671f": "2023-07-31 14:19:08,683"} {"name": "example", "filename": "aa.py", "lineno": 40, "process": 8148, "message": "\u8fde\u63a5\u6570\u636e\u5e93\u201cmy_db\u201d\u5931\u8d25", "exc_info": "Traceback (most recent call last):\n File \"D:\\project\\knowledge-api\\aa.py\", line 37, in <module>\n raise Exception(\"\u8fde\u63a5\u6570\u636e\u5e93\u201cmy_db\u201d\u5931\u8d25\")\nException: \u8fde\u63a5\u6570\u636e\u5e93\u201cmy_db\u201d\u5931\u8d25", "\u7b49\u7ea7": "ERROR", "\u65e5\u671f": "2023-07-31 14:19:08,683"}
|
日志文件输出结果
1 2 3 4 5 6 7 8 9 10
| { "name": "example", "filename": "aa.py", "lineno": 40, "process": 8148, "message": "连接数据库“my_db”失败", "exc_info": "Traceback (most recent call last):\n File \"D:\\project\\knowledge-api\\aa.py\", line 37, in <module>\n raise Exception(\"连接数据库“my_db”失败\")\nException: 连接数据库“my_db”失败", "等级": "ERROR", "日期": "2023-07-31 14:19:08,683" }
|
还可以添加额外的附属信息, 例如:
1 2 3 4
| logger.info( "服务器在端口8080上开始侦听", extra={"python_version": 3.10, "os": "linux", "host": "fedora 38"}, )
|
Loguru
1 2 3 4 5 6 7 8 9
| from loguru import logger
logger.trace("Executing program") logger.debug("Processing data...") logger.info("Server started successfully.") logger.success("Data processing completed successfully.") logger.warning("Invalid configuration detected.") logger.error("Failed to connect to the database.") logger.critical("Unexpected system error occurred. Shutting down.")
|
日志等级和结构化输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from loguru import logger import sys
logger.remove(0)
logger.add(sys.stdout, level="INFO", serialize=True)
logger.trace("Executing program") logger.debug("Processing data...") logger.info("Server started successfully.") logger.success("Data processing completed successfully.") logger.warning("Invalid configuration detected.") logger.error("Failed to connect to the database.") logger.critical("Unexpected system error occurred. Shutting down.")
|
这里虽然设置了 json 输出,但是会包含很多无关的冗余信息。可以自定义消息格式,并且在输出时候添加上下文信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| from loguru import logger import sys import json
def serialize(record): subset = { "timestamp": record["time"].timestamp(), "message": record["message"], "level": record["level"].name, "file": record["file"].name, "context": record["extra"], } return json.dumps(subset)
def patching(record): record["extra"]["serialized"] = serialize(record)
logger.remove(0)
logger = logger.patch(patching)
logger.add(sys.stderr, format="{extra[serialized]}")
logger.info("Processing data...")
logger.bind(user_id="USR-1243", doc_id="DOC-2348").debug("Processing document")
|
携带上下文信息
上面代码中使用了 bind 函数来添加上下文信息到日志中,还可以使用它来创建子记录器来记录共享相同上下文的记录:
1 2 3 4 5
| user_log = logger.bind(user_id="USR-1243", doc_id="DOC-2348") user_log.info("添加用户") user_log.debug("检查用户是否重复") user_log.success("添加用户成功")
|
可以看到以下日志中,都添加了上下文信息。
还可以使用 contextualize
方法来为所有日志都添加上公共的字段,例如,下面的代码片段演示了向由于该请求而创建的所有日志添加唯一的请求 ID 属性:
1 2 3 4 5 6 7 8 9 10 11 12 13
| from loguru import logger import uuid
def logging_middleware(get_response): def middleware(request): request_id = str(uuid.uuid4())
with logger.contextualize(request_id=request_id): response = get_response(request) response["X-Request-ID"] = request_id return response
return middleware
|
Loguru 还为来自标准模块的用户提供了迁移指南。
参考:https://betterstack.com/community/guides/logging/best-python-logging-libraries/